"""
传输720P30帧时延需求一般的音视频渲染流
注意：电脑重启后ip可能会变。
"""
import base64
import math
import os
import platform
import struct
import sys
import threading
import time

import cv2
import numpy as np
import socket

timelist = []
allspeedlist = []  # 仅做测试显示 带宽总变化情况
speedlist = []
dll = []
jitlist = []
pll = []

is_slice = 0
# 222.20.74.225  192.168.125.1
aimIp = "222.20.75.49"  # 端口见ipconfig 需要往自己电脑发。
recv_count = 0.0  # 统计速率
DEBUG = 1
g_th_flag = True  # 理论上是标记线程是否关闭的标识。但似乎因为recv的阻塞而无法实现？
BUFF_SIZE = 65536
testprint = 2


def socket_build(ipaddr, port, isUdp):
    """
    建立socket通信的函数，会同时和切片渲染服务器和切片策略服务器展开通信(4.20改为TCP 为了可以按port切片)
    :param ipaddr: 目的ip，即切片策略服务器or切片渲染服务器的ip，对应虚拟机的ip（实质是本机ip 通过端口映射进去）
    :param port: 切片策略服务器或者切片渲染服务器的port，发送来定位
    # :param isUdp: 是往切片渲染服务器还是切片策略服务器发送（即是udp还是tcp）
    :return: 如果是切片策略服务器通信，则是绑定好的socket s。如果是切片渲染服务器，则是
    """
    try:
        if isUdp == 1:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, BUFF_SIZE)
            s.sendto('Start video'.encode('utf-8'), (ipaddr, port))  # 往渲染服务器发送消息
            print('send:', 'Start video', (ipaddr, port))
            print(s.getsockname()[1])
            ss = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            ss.bind((aimIp, s.getsockname()[1] + 1))
            print('rcv socket port: ', ss.getsockname()[1])
            ss.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, BUFF_SIZE)
            return ss
        else:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.connect((ipaddr, port))
    except socket.error as msg:
        print(msg)
        sys.exit(1)
    return s


# 两个list转换的工具类函数
def iListTStr(l):
    lstr = [str(x) for x in l]
    return ', '.join(lstr)


def strListTf(l):
    li = [float(x) for x in l]
    return li


# 按需分类模块：
# 返回整型表示的用户需求
def getReq(resol, fps, gameType):
    if resol == 720 and fps == 30:  # 低带宽
        if gameType:  # 低时延
            return 2
        else:
            return 0
    elif resol == 1080 or fps == 60:  # 还没想好如何界定
        if gameType:
            return 3
        else:
            return 1
    return -1


# 网络测量模块：
# 函数1：测量带宽(kbps)
def getSpeed():
    global recv_count, speedlist  # 存的是float形式
    time.sleep(1)  # 先让渲染通信进入正常状态
    while g_th_flag:
        recv_count = 0.0
        time.sleep(1)
        recv_count = recv_count / 1000 * 8  # 速率领域的kbps似乎是1000进制！*8是因为uint8是一个字节，其array的len*8即为bits数
        speedlist.append(recv_count)
        allspeedlist.append(recv_count)
        if len(speedlist) > 120:
            speedlist.pop(0)
        print(f'speed:{recv_count} kbps')


# 网络测量函数2：测量时延丢包率
def getDlPl():
    global pll, dll, aimIp
    time.sleep(1)  # 与speed同步
    while g_th_flag:
        time.sleep(1)
        param = '-n' if platform.system().lower() == 'windows' else '-c'
        result = os.popen('ping {} 1 {}'.format(param, aimIp)).read()
        # result = os.popen('ping {} 1 {}'.format(param, 'www.baidu.com')).read()
        # print(f'res:{result}')
        plstart = '丢失 = '
        pls = result.find(plstart)
        ple = result.find(' (', pls, pls + 10)
        los = result[pls + len(plstart):ple]
        pll.append(los)
        if len(pll) > 120:
            pll.pop(0)
        # print(f'los:{los}') 
        # 读取时延
        dlStart = '平均 = '
        dls = result.find(dlStart)
        dle = result.find('ms', dls, dls + 10)
        dl = result[dls + len(dlStart):dle]
        dll.append(dl)
        if len(dll) > 120:
            dll.pop(0)
        # print(f'delay:{dl}') 


# 网络测量函数3：计算抖动
def calJit():  # 存的是str形式
    global jitlist, dll
    i = 1
    t = [0]  # 初始第一项为0
    while i < len(dll):
        t.append(math.fabs(int(dll[i - 1]) - int(dll[i])))
        i += 1
    jitlist = [str(x) for x in t]


# 和策略服务器定时通信（目前实验全需要切片）。req是需求类型，tp表示是建立切片的发送还是已建立切片只上传qoe的
# port是执行渲染流通信的端口。
def sendToSli(ssl, user_ip, user_port):
    req = getReq(720, 30, 1)  # 需求高带宽低时延的最高型3

    global speedlist, dll, jitlist, pll
    tp = 1
    str1 = ', BandWidth=['
    str2 = ', Delay=['
    str3 = ', Jitter=['
    str4 = ', PacketLoss=['
    # time.sleep(10)
    while g_th_flag:
        time.sleep(10)  # 由于阻塞式停止10s 其实退出的时候需要break？
        if not g_th_flag:
            break
        msg = ''
        calJit()
        if len(pll):
            pl = sum(strListTf(pll)) / len(pll)
        else:
            pl = 0.0
        # print(f'{len(speedlist)}, {len(delaylist)}, {len(jitlist)}, {len(pllist)}')
        msg = msg + '{type=' + str(tp) + str1 + iListTStr(speedlist) + ']' + str2 + iListTStr(dll) + ']' + str3 + \
              iListTStr(jitlist) + ']' + str4 + str(pl) + '], ' + 'req=' + str(req) + '}, ' + 'port:' + \
              str(user_port) + ', END'  # END是结尾
        ssl.sendall(msg.encode())

        # 清空原网络质量情况    
        speedlist.clear()
        dll.clear()
        jitlist.clear()
        pll.clear()
        if tp == 1:  # 只有第一次是切片请求，后面的是网络测量通报
            tp = 0
    tp = 2
    msg = msg + '{type=' + str(tp) + str1 + ']' + str2 + ']' + str3 + ']' + str4 + '], ' + 'req=' + str(req) + '}, ' + \
          'port:' + str(user_port) + ', END'  # END是结尾
    ssl.sendall(msg.encode()) # 释放切片资源
    ssl.close()
    print("成功结束控制连接")


# 和渲染服务器通信的函数
def video_stream(video_socket):  # 接收视频流
    global recv_count, g_th_flag, testprint  # 统计带宽
    cv2.namedWindow('RECEIVING VIDEO')
    cv2.moveWindow('RECEIVING VIDEO', 500, 450)
    fps, st, frames_to_count, cnt = (0, 0, 10, 0)
    payload_size = struct.calcsize("Q")  # 一个unsigned long long结构的大小（无符号8字节整数 返回8）
    data = b""
    while g_th_flag:
        packet, _ = video_socket.recvfrom(BUFF_SIZE)
        viddata = base64.b64decode(packet,' /')
        npdata = np.fromstring(viddata, dtype=np.uint8)
        recv_count += len(npdata)

        frame = cv2.imdecode(npdata,1)
        frame = cv2.putText(frame,'FPS: '+str(fps),(10,40),cv2.FONT_HERSHEY_SIMPLEX,0.7,(0,0,255),2)
        cv2.imshow("RECEIVING VIDEO",frame)

        key = cv2.waitKey(1) & 0xFF
        if key == ord('q'):
            ss_soc.sendall('type=2'.encode())
            time.sleep(0.1)
            g_th_flag = False
            # video_socket.close()
            print('客户端结束运行')
            print('历史速率', allspeedlist)
            # os._exit(1)
            break

        if cnt == frames_to_count:  # 刷新显示的FPS
            try:
                fps = round(frames_to_count / (time.time() - st))
                st = time.time()
                cnt = 0
            except:
                pass
        cnt += 1
    video_socket.close()
    print("成功结束渲染连接")
    cv2.destroyAllWindows()


if __name__ == '__main__':
    # 初始化
    ss_soc = socket_build(aimIp, 5050, 0)  # 策略服务器
    vid_soc = socket_build(aimIp, 8080, 1)  # 渲染服务器用于接收的socket
    # 预先感知文件长度
    # 开启网络质量监测  已经是接收的socket了
    user_ip, user_port = vid_soc.getsockname()[0], vid_soc.getsockname()[1]
    print(user_ip, user_port)
    t1 = threading.Thread(target=getSpeed)
    t1.setDaemon(True)  # 能随着control-c一起退出
    t1.start()
    t2 = threading.Thread(target=getDlPl)
    t2.setDaemon(True)  # 守护线程是一种特殊的线程，它的作用是为其他非守护线程提供服务，当所有的非守护线程都结束时，程序会自动退出，
    # 而不理会守护线程是否已经完成。
    t2.start()
    # 进入通信
    try:
        t3 = threading.Thread(target=video_stream, args=(vid_soc,))
        t4 = threading.Thread(target=sendToSli, args=(ss_soc, user_ip, user_port,))
        # t3.setDaemon(True)
        # t4.setDaemon(True)
        t3.start()
        t4.start()
        t1.join()
        t2.join()
        t3.join()
        t4.join()  # 如果没有调用 join，主线程可能会在工作者线程完成之前就结束了
    except KeyboardInterrupt as e:
        g_th_flag = False
        # ss_soc.sendall('type=2'.encode())
        print('客户端结束运行')
        print('线程已全部退出')
        os._exit(0)
